Skip to content

Prototype bound instruments#8314

Draft
jack-berg wants to merge 15 commits intoopen-telemetry:mainfrom
jack-berg:prototype-bound-instruments-1
Draft

Prototype bound instruments#8314
jack-berg wants to merge 15 commits intoopen-telemetry:mainfrom
jack-berg:prototype-bound-instruments-1

Conversation

@jack-berg
Copy link
Copy Markdown
Member

@jack-berg jack-berg commented Apr 21, 2026

Builds off of #8308, #8313.

Related to open-telemetry/opentelemetry-specification#4126

Usage example:

  LongCounter rolls =
      meter
          .counterBuilder("dice.rolls")
          .setDescription("The number of times each side of the die was rolled")
          .setUnit("{roll}")
          .build();

  // Bind one LongCounter per die face. Each bind() call resolves the underlying timeseries
  // once, so subsequent add() calls record directly without any attribute lookup.
  //
  // Equivalent unbound setup (no bind calls needed, but per-recording overhead is higher):
  //   // no setup — just call rolls.add(1, ROLL_N) inline below
  LongCounter face1 = rolls.bind(ROLL_1);
  LongCounter face2 = rolls.bind(ROLL_2);
  LongCounter face3 = rolls.bind(ROLL_3);
  LongCounter face4 = rolls.bind(ROLL_4);
  LongCounter face5 = rolls.bind(ROLL_5);
  LongCounter face6 = rolls.bind(ROLL_6);

  // Simulate 600 rolls with a fixed seed for a reproducible distribution.
  Random random = new Random(42);
  long[] counts = new long[7]; // indexed 1..6; index 0 unused

  for (int i = 0; i < 600; i++) {
    int result = random.nextInt(6) + 1;
    counts[result]++;
    switch (result) {
      case 1:
        face1.add(1);
        // Equivalent unbound: rolls.add(1, ROLL_1);
        break;
      case 2:
        face2.add(1);
        // Equivalent unbound: rolls.add(1, ROLL_2);
        break;
      case 3:
        face3.add(1);
        // Equivalent unbound: rolls.add(1, ROLL_3);
        break;
      case 4:
        face4.add(1);
        // Equivalent unbound: rolls.add(1, ROLL_4);
        break;
      case 5:
        face5.add(1);
        // Equivalent unbound: rolls.add(1, ROLL_5);
        break;
      case 6:
        face6.add(1);
        // Equivalent unbound: rolls.add(1, ROLL_6);
        break;
      default:
        break;
    }
  }

MetricRecordBenchmark has been updated with a new isBound=true|false parameter. The following characterizes the change in performance from isBound=false to isBound=true:

Threads Temporality Cardinality Instrument false (ops/s) true (ops/s) Δ ops/s Δ %
1 DELTA 1 COUNTER_SUM 121,089,075 192,205,716 +71,116,641 +58.7%
1 DELTA 1 UP_DOWN_COUNTER_SUM 118,851,858 192,812,931 +73,961,073 +62.2%
1 DELTA 1 GAUGE_LAST_VALUE 36,844,745 50,932,420 +14,087,675 +38.2%
1 DELTA 1 HISTOGRAM_EXPLICIT 65,486,106 126,279,243 +60,793,137 +92.8%
1 DELTA 1 HISTOGRAM_BASE2_EXPONENTIAL 43,699,813 50,554,686 +6,854,873 +15.7%
1 DELTA 128 COUNTER_SUM 88,432,937 174,938,224 +86,505,287 +97.8%
1 DELTA 128 UP_DOWN_COUNTER_SUM 98,779,351 173,625,923 +74,846,572 +75.8%
1 DELTA 128 GAUGE_LAST_VALUE 30,131,568 40,321,751 +10,190,183 +33.8%
1 DELTA 128 HISTOGRAM_EXPLICIT 67,302,909 100,070,564 +32,767,655 +48.7%
1 DELTA 128 HISTOGRAM_BASE2_EXPONENTIAL 40,322,250 44,477,207 +4,154,957 +10.3%
1 CUMULATIVE 1 COUNTER_SUM 165,108,490 217,696,971 +52,588,481 +31.9%
1 CUMULATIVE 1 UP_DOWN_COUNTER_SUM 167,649,049 217,762,066 +50,113,017 +29.9%
1 CUMULATIVE 1 GAUGE_LAST_VALUE 52,796,918 69,523,095 +16,726,177 +31.7%
1 CUMULATIVE 1 HISTOGRAM_EXPLICIT 98,599,075 134,232,124 +35,633,049 +36.1%
1 CUMULATIVE 1 HISTOGRAM_BASE2_EXPONENTIAL 47,987,710 52,049,465 +4,061,755 +8.5%
1 CUMULATIVE 128 COUNTER_SUM 97,173,597 208,753,309 +111,579,712 +114.8%
1 CUMULATIVE 128 UP_DOWN_COUNTER_SUM 89,467,232 212,524,196 +123,056,964 +137.5%
1 CUMULATIVE 128 GAUGE_LAST_VALUE 84,604,974 178,211,282 +93,606,308 +110.6%
1 CUMULATIVE 128 HISTOGRAM_EXPLICIT 72,568,957 113,979,337 +41,410,380 +57.1%
1 CUMULATIVE 128 HISTOGRAM_BASE2_EXPONENTIAL 42,047,155 48,461,390 +6,414,235 +15.3%
4 DELTA 1 COUNTER_SUM 15,783,456 15,971,961 +188,505 +1.2%
4 DELTA 1 UP_DOWN_COUNTER_SUM 15,553,055 15,768,805 +215,750 +1.4%
4 DELTA 1 GAUGE_LAST_VALUE 16,912,782 17,007,491 +94,709 +0.6%
4 DELTA 1 HISTOGRAM_EXPLICIT 11,529,153 12,525,305 +996,152 +8.6%
4 DELTA 1 HISTOGRAM_BASE2_EXPONENTIAL 10,225,731 10,859,416 +633,685 +6.2%
4 DELTA 128 COUNTER_SUM 69,836,518 64,232,111 -5,604,407 -8.0%
4 DELTA 128 UP_DOWN_COUNTER_SUM 70,537,733 68,112,254 -2,425,479 -3.4%
4 DELTA 128 GAUGE_LAST_VALUE 52,568,235 50,161,960 -2,406,275 -4.6%
4 DELTA 128 HISTOGRAM_EXPLICIT 57,684,667 54,039,908 -3,644,759 -6.3%
4 DELTA 128 HISTOGRAM_BASE2_EXPONENTIAL 53,421,058 61,206,232 +7,785,174 +14.6%
4 CUMULATIVE 1 COUNTER_SUM 71,761,470 63,659,613 -8,101,857 -11.3%
4 CUMULATIVE 1 UP_DOWN_COUNTER_SUM 80,094,271 64,277,355 -15,816,916 -19.7%
4 CUMULATIVE 1 GAUGE_LAST_VALUE 28,402,730 29,473,657 +1,070,927 +3.8%
4 CUMULATIVE 1 HISTOGRAM_EXPLICIT 18,520,440 22,887,712 +4,367,272 +23.6%
4 CUMULATIVE 1 HISTOGRAM_BASE2_EXPONENTIAL 20,075,789 18,163,404 -1,912,385 -9.5%
4 CUMULATIVE 128 COUNTER_SUM 114,683,919 121,608,162 +6,924,243 +6.0%
4 CUMULATIVE 128 UP_DOWN_COUNTER_SUM 114,126,653 120,561,192 +6,434,539 +5.6%
4 CUMULATIVE 128 GAUGE_LAST_VALUE 105,673,512 108,782,851 +3,109,339 +2.9%
4 CUMULATIVE 128 HISTOGRAM_EXPLICIT 75,498,415 82,719,884 +7,221,469 +9.6%
4 CUMULATIVE 128 HISTOGRAM_BASE2_EXPONENTIAL 72,549,782 75,801,079 +3,251,297 +4.5%

Modest to large gains across the board, with larger gains for cases with reduced contention and cumulative temporality, where the map lookup represents a larger share of the time to record.

Leaving as draft because:

  1. Need to land a spec PR first
  2. Need to restructure to move the API incubator

@otelbot otelbot Bot added the api-change Changes to public API surface area label Apr 21, 2026
@otelbot
Copy link
Copy Markdown
Contributor

otelbot Bot commented Apr 21, 2026

⚠️ API changes detected — additional maintainer review required

@jack-berg @jkwatson

This PR modifies the public API surface area of the following module(s):

  • opentelemetry-api

Please review the changes in docs/apidiffs/current_vs_latest/ carefully before approving.

@dashpole
Copy link
Copy Markdown
Contributor

Threads Temporality Cardinality Instrument false (ops/s) true (ops/s) Δ ops/s Δ %
1 DELTA 1 COUNTER_SUM 118,208,794 130,599,635 † +12,390,841 +10.5%

For the bound=false case, 1/118,208,794 = 8 ns. Dang. Are java concurrent map lookups just that fast?

@jack-berg
Copy link
Copy Markdown
Member Author

For the bound=false case, 1/118,208,794 = 8 ns. Dang. Are java concurrent map lookups just that fast?

That case has no concurrency (threads=1). We optimize map lookups slightly by caching the hashcode of our Attribute implementation.

That number (and all of them frankly) is suspiciously fast, so I'm double checking things. Things are checking out initially. There is an issue with the cardinality=1 case, where its possible the JIT compiler is lifting hositing the map lookup, but its possible the JIT compiler could do that in a real application in a cardinality=1 case as well, so not wrong per say. But even the cardinality=128 cases where a JIT hoist is unlikely are blazing fast so speed can't be only attributed to JIT.

I ran those benchmarks on my mac mini, which uses apple m4 chip. Currently on the main branch, running on the dedicated benchmark bare metal hardware, that same series gets 29340717 ops/s, or 34ns. Which is fast but more believable. Maybe apple silicon / ARM architecture is exceptionally fast for these types of benchmarks.

https://open-telemetry.github.io/opentelemetry-java/benchmarks/
But you'll probably have to go to the raw data backing those graphs because they're currently pretty unusable for zooming in on a specific series and copying figures: https://raw.githubusercontent.com/open-telemetry/opentelemetry-java/refs/heads/benchmarks/benchmarks/data.js

If you spot any problems with the methodology of MetricRecordBenchmark, please let me know.

@dashpole
Copy link
Copy Markdown
Contributor

Yeah, I couldn't find any problem with the methodology or the code. For Go, the map lookup takes ~20 ns, and ~45 ns with high concurrency, and the atomic counter increment takes < 10 ns, so we see a bigger difference. I was curious if you had any tricks up your sleeve, or if java maps were just faster

@jack-berg
Copy link
Copy Markdown
Member Author

I was curious if you had any tricks up your sleeve, or if java maps were just faster

I was curious about the map lookup perf as well, so created a dedicated benchmark based on it: jack-berg@b9cf4c4

Parameters I test:

  • Concurrent access: 1 or 4 threads
  • Cardinality (size of map): 1, 128, 1024
  • Size of map keys: small (~126 char key), medium (~1026 char keys), large (~100x26 char keys)
  • Key type: string (plain ole string), attributes_cached (java attr impl w/ cached hashCode), attributes_uncached (java attrs impl w/o cached hashCode)

Results:

threads=1 — ns/op

keySize cardinality STRING ATTR_CACHED ATTR_UNCACHED
SMALL 1 1.4 2.4 3.7
SMALL 128 2.1 6.6 7.3
SMALL 1024 2.1 6.5 8.6
MEDIUM 1 1.4 2.5 13.7
MEDIUM 128 2.4 7.0 19.5
MEDIUM 1024 2.8 7.0 26.4
LARGE 1 1.4 2.5 157.2
LARGE 128 8.8 11.0 179.2
LARGE 1024 9.5 10.9 186.8

threads=4 — (4-thread aggregate; multiply by 4 for per-thread cost)

keySize cardinality STRING ATTR_CACHED ATTR_UNCACHED
SMALL 1 0.7† 0.7† 0.9
SMALL 128 1.7 1.7 1.8
SMALL 1024 0.9††† 1.7 2.1
MEDIUM 1 0.8† 0.8† 3.3
MEDIUM 128 1.9 1.8 5.0
MEDIUM 1024 1.9 1.9 6.8
LARGE 1 0.7† 0.8† 39.2
LARGE 128 3.3 2.7 44.7
LARGE 1024 3.7 2.9 46.7

† ±11–16% variance ††† ±46% variance (discard)

So lookups are really fast. Caching hashCodes matters a lot, especially as the size of keys becomes larger (this is intuitive). Cardinality matters a little bit, but not as much as key size. I only tested up to 1024, but given that default cardinality limit is 2k, I think this reasonably represents the use case.

Taking these conclusions back to bound instruments, I think the benchmark setup I have for MetricRecordBenchmark is reasonable. The cardinality is small (128) and attributes are small (just 1*26 char key), but since we cache hashCode, those don't matter that much. I could increase cardinality and attribute size to increase the positive impact of bound instruments.

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 30, 2026

Codecov Report

❌ Patch coverage is 72.16749% with 113 lines in your changes missing coverage. Please review.
✅ Project coverage is 89.98%. Comparing base (fdb8194) to head (1e5b7d5).
⚠️ Report is 17 commits behind head on main.

Files with missing lines Patch % Lines
...in/java/io/opentelemetry/sdk/metrics/SdkMeter.java 0.00% 10 Missing ⚠️
...ntelemetry/sdk/metrics/SdkDoubleUpDownCounter.java 43.75% 9 Missing ⚠️
...ava/io/opentelemetry/api/metrics/DefaultMeter.java 0.00% 8 Missing ⚠️
...ry/api/incubator/metrics/ExtendedDefaultMeter.java 0.00% 8 Missing ⚠️
...sdk/metrics/internal/state/EmptyMetricStorage.java 0.00% 8 Missing ⚠️
...io/opentelemetry/sdk/metrics/SdkDoubleCounter.java 50.00% 7 Missing ⚠️
...a/io/opentelemetry/sdk/metrics/SdkDoubleGauge.java 50.00% 7 Missing ⚠️
.../opentelemetry/sdk/metrics/SdkDoubleHistogram.java 50.00% 7 Missing ⚠️
...a/io/opentelemetry/sdk/metrics/SdkLongCounter.java 50.00% 7 Missing ⚠️
...io/opentelemetry/sdk/metrics/SdkLongHistogram.java 50.00% 7 Missing ⚠️
... and 14 more

❌ Your patch check has failed because the patch coverage (72.16%) is below the target coverage (80.00%). You can increase the patch coverage or adjust the target coverage.

Additional details and impacted files
@@             Coverage Diff              @@
##               main    #8314      +/-   ##
============================================
- Coverage     90.27%   89.98%   -0.30%     
- Complexity     7693     7795     +102     
============================================
  Files           850      852       +2     
  Lines         23207    23533     +326     
  Branches       2356     2380      +24     
============================================
+ Hits          20951    21176     +225     
- Misses         1530     1629      +99     
- Partials        726      728       +2     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api-change Changes to public API surface area

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants